VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
  Persistable = 0  'NotPersistable
  DataBindingBehavior = 0  'vbNone
  DataSourceBehavior  = 0  'vbNone
  MTSTransactionMode  = 0  'NotAnMTSObject
END
Attribute VB_Name = "cPNGwriter"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

' This class is a partial version of a full-blown PNG creation class.
' It creates a PNG with many options, but not nearly all PNG options avaialble
' and is specifically modified to creating PNGs from pre-multiplied 32bpp DIBs.
' There still may be references to Interlacing. Interlacing options do not
' exist in this class and any such references are results of extracting the
' routines from the full-version PNG creation class.
' Note that GDI+ does not offer any PNG options when creating PNGs, this class
' exposes several options and can be modified to support all PNG options.

' CUSTOM TAILORED FOR PRE-MULTIPLIED 32bpp DIBS. Routines not portable for normal DIBs.

' Required is a version of the zLIB DLL which can be found at www.zlib.net.
' zLIB comes in at least two varieties: C calling convention (_cdecl) and
' VB/PASCAL calling convention (_stdcall). This routine can use either of those
' conventions, but the zLIB file must be named one of the two following,
' not case sensitive, both are original filenames:  zLib.dll or zLib1.dll

' Key highlights:
' 1. PNGs can be created without GDI+ as long as zlib or zlib1 is present
' 2. Using bit reduction algorithms, a 32bpp DIB can be converted to one of the
'       the following:  8 bpp paletted, 24 bpp or 32 bpp PNGs; supporting full alpha
' 3. The PNG compression filtering mechanism in this routine is user-selected.
'       Filters assist in reorganizing byte information to make it compress better
'       Speed vs Size tradeoffs: filter type None is fastest while type Paeth is smallest (generally)
'       See notes in FilterImage routine, set filter in c32bppDIB.PngPropertySet routine
' 4. Over a dozen options available when creating PNGs, see Me.AddProperty
' 5. This class almost always creates smaller PNG files than GDI+ when default filtering is used
' 6. PNGs can be saved to file or saved to an array



' array mapping structures
Private Type SafeArrayBound
    cElements As Long               ' number of array items
    lLbound As Long                 ' the LBound of the array
End Type
Private Type SafeArray
    cDims As Integer                ' numer of dimensions (1) for this UDT
    fFeatures As Integer            ' not used
    cbElements As Long              ' byte size of each element (byte=1,Int=2,Long=4)
    cLocks As Long                  ' not used
    pvData As Long                  ' pointer to memory space containing array
    rgSABound(0 To 1) As SafeArrayBound
End Type

Private Declare Sub CopyMemory Lib "kernel32.dll" Alias "RtlMoveMemory" (ByRef Destination As Any, ByRef Source As Any, ByVal Length As Long)
Private Declare Sub FillMemory Lib "kernel32.dll" Alias "RtlFillMemory" (ByRef Destination As Any, ByVal Length As Long, ByVal Fill As Byte)
' change to msvbvm50.dll for VB5 projects:
Private Declare Function VarPtrArray Lib "msvbvm60.dll" Alias "VarPtr" (Ptr() As Any) As Long

' zLIB calls, needed to compress/decompress png data
' ///////////// ZLIB.DLL REQUIREMENT \\\\\\\\\\\\
' validated via ValidateDLLExists function
Private Declare Function Zcrc32 Lib "zlib.dll" Alias "crc32" (ByVal crc As Long, ByRef buf As Long, ByVal Length As Long) As Long
Private Declare Function Zcompress Lib "zlib.dll" Alias "compress" (ByRef Dest As Long, ByRef destLen As Long, ByRef Source As Long, ByVal sourceLen As Long) As Long
Private Declare Function Zcompress2 Lib "zlib.dll" Alias "compress2" (ByRef Dest As Long, ByRef destLen As Long, ByRef Source As Long, ByVal sourceLen As Long, ByVal Level As Long) As Long

Private Declare Function Zcrc321 Lib "zlib1.dll" Alias "crc32" (ByVal crc As Long, ByRef buf As Any, ByVal Length As Long) As Long
Private Declare Function Zcompress1 Lib "zlib1.dll" Alias "compress" (ByRef Dest As Long, ByRef destLen As Long, ByRef Source As Long, ByVal sourceLen As Long) As Long
Private Declare Function Zcompress21 Lib "zlib1.dll" Alias "compress2" (ByRef Dest As Long, ByRef destLen As Long, ByRef Source As Long, ByVal sourceLen As Long, ByVal Level As Long) As Long
Private Const zlibMaxCompression = 9

Private Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long, lpOverlapped As Any) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Const INVALID_HANDLE_VALUE = -1

Private Enum eColorTypes    ' internal use only
    clrGrayScale = 0
    clrTrueColor = 2
    clrPalette = 3
    clrGrayAlpha = 4
    clrTrueAlpha = 6
End Enum

'----------------------------------------------------------------------------
' following are optional PNG properties only
Private m_Filter As eFilterMethods
Private m_bKGD As Long          ' default PNG background color if a view opts to render against solid bkg
Private m_Captions() As String  ' see c32bppDIB.ePngProperties
Private m_PNGprops As Long      ' indicates which, if any, m_Captions are used
'----------------------------------------------------------------------------

Private cCfunction As cCDECL        ' class to allow using C calling convention
Private m_ZLIBver As Long           ' which version of zLIB?
Private m_Palette() As Byte         ' PNG palette if image can be palettized
Private m_transPal() As Byte        ' alpha values for PNG palettes as needed
Private m_Uncompressed() As Byte    ' initialized, contains uncompressed DIB bytes in 8,24,32 bit formats
Private m_Stream() As Byte          ' never initialized, overlay to host 32bpp DIB
Private m_Trans As Long             ' flag indicating whether or not transparency is used in DIB
Private m_ColorType As eColorTypes  ' the color type the PNG will be created in

Friend Function SavePNGex(cHost As c32bppDIB, FileName As String, outStream() As Byte) As Boolean

    If cHost.Handle = 0& Then Exit Function
    
    Dim tSA As SafeArray    ' overlay onto our DIB as needed
    Dim bSuccess As Boolean
    Dim fileNum As Integer
    Dim hFile As Long
    Dim bSkipBKGD As Boolean
    
    ' if we don't have Zlib, we can't continue with this class
    If zValidateZLIBversion = False Then Exit Function
    
    If Not FileName = vbNullString Then
        hFile = iparseGetFileHandle(FileName, False)
        If (hFile = INVALID_HANDLE_VALUE) Then Exit Function
    End If
    
    With tSA                ' overlay DIB
        .cbElements = 1
        .cDims = 2
        .pvData = cHost.BitsPointer
        .rgSABound(0).cElements = cHost.Height
        .rgSABound(1).cElements = cHost.scanWidth
    End With
    CopyMemory ByVal VarPtrArray(m_Stream), VarPtr(tSA), 4&
    
    ' optimizations to reduce bit depth and reduce palette data
    
     On Error GoTo ExitRoutine
    ' Can image be palettized (smallest PNG size)?
    m_Trans = -1&
    If PalettizeImage(cHost.Alpha) = False Then
        ' if not, can we reduce to 24bpp from 32bpp?
        OptimizeTrueColor cHost.Alpha
    End If
    '  The above functions converted 32bpp DIB to necessary format for PNG creation
    ' The conversion is in the m_uncompress array. Remove overlay now
    CopyMemory ByVal VarPtrArray(m_Stream), 0&, 4&
    tSA.cDims = 0
    
''   CREATE THE PNG using following rules
''   ------------------------------------
''    Critical chunks (must appear in this order):
''
''               Name  Multiple Ok?  Ordering constraints
''               IHDR    No          Must be first
''               PLTE    No          Before first IDAT (chunk is optional)
''               IDAT    Yes         Multiple IDATs must be consecutive
''               IEND    No          Must be last
''
''    Ancillary chunks (among other ancilliary chunks, order is not dictated):
''
''               Name  Multiple OK?  Ordering constraints relative to Critical chunks
''               cHRM    No          Before PLTE and IDAT
''               gAMA    No          Before PLTE and IDAT
''               iCCP    No          Before PLTE and IDAT
''               sBIT    No          Before PLTE and IDAT
''               sRGB    No          Before PLTE and IDAT
''               bKGD    No          After PLTE; before IDAT
''               hIST    No          After PLTE; before IDAT
''               tRNS    No          After PLTE; before IDAT
''               pHYs    No          Before IDAT
''               sPLT    Yes         Before IDAT
''               tIME    No          None
''               iTXt    Yes         None
''               tEXt    Yes         None
''               zTXt    Yes         None
    
    ' Write the PNG header
    If Write_IHDR(hFile, outStream, cHost, False) = False Then GoTo ExitRoutine
    If Write_tEXt(hFile, outStream, True) = False Then GoTo ExitRoutine ' write the Author & Title if needed
    If Write_PLTE(hFile, outStream, bSkipBKGD) = False Then GoTo ExitRoutine ' write the palette
    If Not bSkipBKGD Then   ' < may be set when bkgd color is not part of palette (Color Type 3 only)
        If Write_bKGD(hFile, outStream) = False Then GoTo ExitRoutine ' write bkgd color
    End If
    If Write_tRNS(hFile, outStream) = False Then GoTo ExitRoutine ' write transparency info
    ' Here we are going to filter & compress the DIB data & then write the IDAT chunk
    If FilterImage(hFile, outStream, cHost, m_Filter) = False Then GoTo ExitRoutine ' write data
    If Write_tIMe(hFile, outStream) = False Then GoTo ExitRoutine ' write last modified timestamp
    If Write_tEXt(hFile, outStream, False) = False Then GoTo ExitRoutine ' write other text (i.e., description, etc)
    If Write_zTXt(hFile, outStream) = True Then ' write any miscellaneous text
        ' Add the IEND termination to the PNG
        bSuccess = Write_IEND(hFile, outStream)   ' write the end flag
    End If

ExitRoutine:
    ' clean up as needed

    If Not tSA.cDims = 0 Then CopyMemory ByVal VarPtrArray(m_Stream), 0&, 4&
    If Not hFile = 0& Then CloseHandle hFile
    
    Erase m_transPal()
    Erase m_Palette()
    Erase m_Uncompressed()
    Set cCfunction = Nothing
    
    If Err Then Err.Clear
    On Error Resume Next
    If bSuccess = False Then
        If hFile = 0& Then Erase outStream() Else iparseDeleteFile FileName
    Else
        SavePNGex = bSuccess
    End If
    If Err Then Err.Clear

End Function

Private Function PropertyIndex(PropertyID As ePngProperties, Optional LargeBlockCaption As String) As Long

    ' Helper function. Returns the m_Captions() array index for the passed PropertyID

    Dim x As Long, CaptionID As Long
    
    If PropertyID = txtLargeBlockText Then
        For CaptionID = 11 To UBound(m_Captions)
            x = InStr(m_Captions(CaptionID), Chr$(0))
            If StrComp(Left$(m_Captions(CaptionID), x - 1&), LargeBlockCaption) = 0& Then Exit For
        Next
        If CaptionID > UBound(m_Captions) Then CaptionID = -1&
    Else
        x = PropertyID
        Do Until x = 1&
            x = x \ 2&
            CaptionID = CaptionID + 1&
        Loop
    End If

    PropertyIndex = CaptionID

End Function

Private Function OptimizeTrueColor(ByVal isAlpha As Boolean) As Boolean

    ' Function attempts to reduce 32bpp DIB to 24bpp DIB.
    ' Reduction to Palette already tried before this routine was called
    ' Reduction can occur when:
    '   1. No transparency is used
    '   2. Only one color is fully transparent (if alpha between 1 & 254 then no reduction)
    
    Dim x As Long, y As Long
    Dim bAbort As Boolean, tOffset As Long
    Dim palAlpha(0 To 255) As Byte, palCount As Long
    Dim scanWidth As Long, Color As Long
    
    m_Trans = -1&       ' flag indicating no simple transparency. ColorType 6 implies transparency
    If isAlpha Then
        ' we will test if only full transparency is used, and only one color uses transparency.
        ' We don't need to determine which color is transparent, because with pre-multiplied
        ' DIBs, the color is always black: 0,0,0. But if another color is transparent, can't reduce
        
        m_ColorType = clrTrueAlpha  ' default color type for this DIB
        For y = 0& To UBound(m_Stream, 2)
            For x = 3& To UBound(m_Stream, 1) Step 4&
                ' look at alpha values, if any are semi-transparent, abort
                If m_Stream(x, y) = 0 Then ' full transparency
                    ' If color is not black, we abort
                    If Not (m_Stream(x - 3, y) = 0) Then
                        bAbort = True: Exit For
                    ElseIf Not (m_Stream(x - 2, y) = 0) Then
                        bAbort = True: Exit For
                    ElseIf Not (m_Stream(x - 1, y) = 0) Then
                        bAbort = True: Exit For
                    End If
                
                ElseIf Not m_Stream(x, y) = 255 Then   ' partial transparency, abort
                    bAbort = True: Exit For
                End If
            Next
            If bAbort Then Exit For
        Next
        If Not bAbort Then ' reduction to 24bpp can be done?
            ' now here's the catch. Black is always transparent in premultiplied DIBs, but if
            ' non-transparent black is used anywhere else in the image, then we can't leave
            ' black as the transparent color; we'll need to change it. This routine will run
            ' quickly. We will not make the effort to check every possible color in the 16 million
            ' color range, rather, we will be looking at just 1024 colors: 256 Reds, 256 Greens,
            ' & 256 Blues. If we find one we can use, bingo, else write as 32bpp
            For Color = 0& To 2&  ' Color = B, G, R
            
                tOffset = 3& - Color ' location of the alpha byte relative to "Color"
                palCount = 0&       ' number of "Color" shades used; from 1 to 256
                
                For y = 0& To UBound(m_Stream, 2)
                    For x = Color To UBound(m_Stream, 1) Step 4&
                    
                        If Not m_Stream(x + tOffset, y) = 0 Then    ' is this our transparent color?
                            If palAlpha(m_Stream(x, y)) = 0 Then    ' no, but has it been counted?
                                palCount = palCount + 1&            ' up the count & abort if we maxed out
                                If palCount = 256& Then
                                    bAbort = True
                                    Exit For
                                End If
                                palAlpha(m_Stream(x, y)) = 1        ' flag it
                            End If
                        End If
                    Next
                    
                    If bAbort Then Exit For ' all 256 shades of "Color" used
                    
                Next
                
                If palCount < 256& Then ' did we find a color we can use?
                    For x = 0& To 255&  ' lets find out which it is
                        If palAlpha(x) = 0 Then
                            ' since the X-shade of the R, G, or B isn't used in the image,
                            ' we can safely state that RGB(X,X,X) is also not in the image
                            m_Trans = x Or x * &H100& Or x * &H10000
                            Exit For
                        End If
                    Next
                    m_ColorType = clrTrueColor  ' reduce to 24bpp vs 32bpp
                    Exit For
                End If
                
                Erase palAlpha()    ' reset to zeros
                bAbort = False      ' reset
                
            Next
        End If
    Else
        m_ColorType = clrTrueColor      ' no transparency, reduction to 24bpp
    End If
    
    ' Use separate loops vs adding an IF statement for every pixel to test for color type
    If m_ColorType = clrTrueAlpha Then  ' 32bpp (ColorType 6)
        scanWidth = UBound(m_Stream, 1) + 1&
        ReDim m_Uncompressed(0 To scanWidth * (UBound(m_Stream, 2) + 1&) - 1&)
        For y = 0& To UBound(m_Stream, 2)
            tOffset = y * scanWidth
            For x = 0& To UBound(m_Stream, 1) Step 4&
                ' simultaneously remove pre-multiplication
                Select Case m_Stream(x + 3&, y)
                Case 255
                    m_Uncompressed(tOffset) = m_Stream(x + 2&, y)
                    m_Uncompressed(tOffset + 1&) = m_Stream(x + 1&, y)
                    m_Uncompressed(tOffset + 2&) = m_Stream(x, y)
                    m_Uncompressed(tOffset + 3&) = 255
                Case 0 ' do nothing
                Case Else
                    Color = m_Stream(x + 3&, y)
                    m_Uncompressed(tOffset) = (255& * m_Stream(x + 2&, y) \ Color)
                    m_Uncompressed(tOffset + 1&) = (255& * m_Stream(x + 1&, y) \ Color)
                    m_Uncompressed(tOffset + 2&) = (255& * m_Stream(x, y) \ Color)
                    m_Uncompressed(tOffset + 3&) = Color
                End Select
                tOffset = tOffset + 4&
            Next
        Next
    Else            ' 24bpp (Color Type 2) with or without simple transparency
        scanWidth = iparseByteAlignOnWord(24, UBound(m_Stream, 1) \ 4 + 1&)
        ' convert BGR to RGB, the Filter function expects 1D arrays
        ReDim m_Uncompressed(0 To scanWidth * (UBound(m_Stream, 2) + 1&) - 1&)
        For y = 0& To UBound(m_Stream, 2)
            tOffset = y * scanWidth
            For x = 0& To UBound(m_Stream, 1) Step 4&
                ' simultaneously remove pre-multiplication. Don't carry over any alpha values
                Select Case m_Stream(x + 3&, y)
                Case 255
                    m_Uncompressed(tOffset) = m_Stream(x + 2&, y)
                    m_Uncompressed(tOffset + 1&) = m_Stream(x + 1&, y)
                    m_Uncompressed(tOffset + 2&) = m_Stream(x, y)
                Case 0 ' uses simple transparency (1 color is transparent)
                    CopyMemory m_Uncompressed(tOffset), m_Trans, 3&
                Case Else
                    Color = m_Stream(x + 3&, y)
                    m_Uncompressed(tOffset) = (255& * m_Stream(x + 2&, y) \ Color)
                    m_Uncompressed(tOffset + 1&) = (255& * m_Stream(x + 1&, y) \ Color)
                    m_Uncompressed(tOffset + 2&) = (255& * m_Stream(x, y) \ Color)
                End Select
                tOffset = tOffset + 3&
            Next
        Next
    End If
End Function

Private Function PalettizeImage(isAlpha As Boolean) As Boolean

    ' Function determines if image can be palettized vs 24/32 bpp true color
    ' Once determined it can be paletted, it will optimize to include the following:
    ' 1. Convert to PNG grayscale palette if possible, saves at least 768 bytes vs color palette
    ' 2. Rearrange palette to reduce alpha/palette entries, saves up to 200+ bytes if alpha is used
    ' 3. Converts per-color grayscale to a modified color palette, reducing size at least 50%
    ' This modified version does not reduce to 1,2,or 4 bits per pixel
    '   -- Any paletted image is 256 colors, but only needed palette entries are cached in PNG
    
    Dim x As Long, y As Long, scanWidth As Long
    Dim palCount As Long, Index As Long
    Dim Color As Long, newColor As Boolean
    Dim palXRef() As Byte, palAlpha() As Byte
    Dim tSortIndex() As Long, tPalette() As Long
    
    On Error GoTo ExitRoutine
    
    ' count unique colors (maximum of 256 if we are to palettize)
    ' Note that alphas are included in the tSortIndex. This is because any color
    ' using more than one alpha value would require separate palette entries:
    ' Example: Red @ Alpha 255 & Red @ Alpha 128 requires two palette entries
    ReDim m_transPal(1 To 256)          ' array to hold alpha values only
    ReDim tSortIndex(1 To 256)          ' sort indexes
    ReDim tPalette(1 To 256) As Long    ' palette
    For y = 0& To UBound(m_Stream, 2)
        For x = 0& To UBound(m_Stream, 1) Step 4&
        
            CopyMemory Color, m_Stream(x, y), 4&
            Index = FindColor(tSortIndex, Color, palCount, newColor)   ' use binary search routine
            If newColor = True Then
                If palCount = 256& Then Exit Function       ' exceeded palette entries limit
                palCount = palCount + 1&                    ' increment entry count & shift palette to maintain asc sort
                If Index < palCount Then
                    CopyMemory tSortIndex(Index + 1&), tSortIndex(Index), (palCount - Index) * 4&
                    CopyMemory tPalette(Index + 1&), tPalette(Index), (palCount - Index) * 4&
                End If
                tSortIndex(Index) = Color                    ' add new color to the palette
                CopyMemory tPalette(Index), Color, 3&
            End If
        
        Next
    Next
    
    ' if we got here, then image can be palettized, but to which of the following?
    ' 1. Palette - no transparency? like non-transparent GIFs (isAlpha=False)
    ' 2. Palette - simple transparency? like transparent GIFs (grayscale handled differently in PNGs)
    ' 3. Palette - per-color index transparency?
    
    y = 0&
    If isAlpha Then
        ' separate alpha from color and count how many non-opaque alpha values
        For x = 1& To palCount
            If (tSortIndex(x) And &H7FFFFFFF) = tSortIndex(x) Then ' high bit not set
                m_transPal(x) = tSortIndex(x) \ &H1000000
            Else                                                 ' high bit is set
                m_transPal(x) = ((tSortIndex(x) And &H7FFFFFFF) \ &H1000000) Or &H80
            End If
            If Not m_transPal(x) = 255 Then
                y = y + 1&   ' count different levels of transparency
                Index = x    ' track last palette entry with alpha value <> 255
            End If
        Next
    Else
        FillMemory m_transPal(1), 256&, 255 ' all alphas are opaque
    End If
    
    Select Case y
    Case 0&
    ' 1. Palette - no transparency? like non-transparent GIFs (isAlpha=False)
        m_Trans = -1& ' no transparency
    Case 1&
    ' 2. Palette - simple transparency? like transparent GIFs
        m_Trans = Index ' flag & may be changed later in this routine
    Case Else
    ' 3. Palette - per-color transparency?
        m_Trans = 0& ' > -1 means we have transparency at some level
        ' alphas are kept in the m_transPal() array
    End Select
    
'     Now for the last optimization attempt: check for grayscale but only for non per-color
'     alpha images. Why restrict grayscale to non per-color alpha when PNG can support grayscale
'     per-color alpha? Here's why: per-color grayscale alpha is ColorType 4. ColorType 4
'     always requires 16 bits per pixel (bpp), regardless of grayscale bit depth, but
'     ColorType 3 requires 8 bpp (max) + 768 palette bytes (max) + 256 bytes (max) for alpha info:
'       ColorType 4, 256x256 image: 256*256*2=131072 bytes for color information (grayscale has no palette in PNGs)
'       ColorType 3 (8bpp), 256x256 image: 256*256*1+768+256=66560 bytes for color information
'           note: ColorType 4 is always 16bpp, but ColorType 3 can be 1,2,4,8 bpp
'           and palette/alpha arrays can be reduced too
    
    m_ColorType = clrPalette    ' Color Type 3 (color palette)
    If Not m_Trans = 0& Then
        ' check each palette entry to see if grayscale or not. When not, abort loop
        For Index = 1& To palCount
            If Not (tPalette(Index) And &HFF) = ((tPalette(Index) \ &H100&) And &HFF) Then ' compare B to G
                Exit For
            ElseIf Not (tPalette(Index) And &HFF) = ((tPalette(Index) \ &H10000) And &HFF) Then ' compare B to R
                Exit For
            End If
        Next
        
        If Index > palCount Then    ' need to tweak transparency possibly
        
            m_ColorType = clrGrayScale ' Color Type 0
            
            If isAlpha = True Then
                ' we only got here because just 1 color was transparent & with a pre-multiplied DIB
                ' that color is always black. But if non-transparent black was used elsewhere in the
                ' grayscale then we need to change the transparency. Non-transparent black is very
                ' common in grayscales
                ReDim palXRef(1 To 256) ' track which grayscales are used
                For x = 1& To palCount
                    If tPalette(x) = 0& Then             ' this is black
                        If Not m_transPal(x) = 0& Then   ' and not our transparent black
                            palXRef(1) = 1     ' mark black as used
                        End If
                    Else
                        palXRef(Index + 1&) = 1 ' non-black, mark as used
                    End If
                Next
                If palXRef(1) = 1 Then
                    ' non-transparent black is used in the grayscale, so we must change our
                    ' tranparent black - Locate a grayscale not in use
                    For Index = 2& To palCount
                        If palXRef(Index) = 0 Then
                            ' bingo, we'll use this one
                            m_Trans = Index - 1&
                            Exit For
                        End If
                    Next
                Else    ' black was not in the image, we can use black as transparency
                    m_Trans = 0&
                End If
                Erase palXRef
            End If
        End If
    End If
    
    scanWidth = UBound(m_Stream, 1) \ 4& + 1&         ' width of image
    x = (UBound(m_Stream, 2) + 1) * scanWidth - 1&   ' calculate size of total image bytes
    ReDim m_Uncompressed(0 To x)    ' the Filter function expects 1D arrays
    
    If m_ColorType = clrGrayScale Then
        ' grayscale is easy enough, transfer 32bpp info to 8bpp info
        ' Remember, PNG grayscale color types do not use palettes.
        ' Grayscale palettes are PNG decoders responsibility
        Erase m_transPal()
        For y = 0& To UBound(m_Stream, 2)
            Index = y * scanWidth
            For x = 0& To UBound(m_Stream, 1) Step 4&
                If m_Stream(x + 3&, y) = 0& Then     ' transparency index needed
                    m_Uncompressed(Index) = m_Trans ' use modified transparency index as necessary
                Else
                    m_Uncompressed(Index) = m_Stream(x, y)  ' use grayscale index
                End If
                Index = Index + 1&
            Next
        Next
        
    Else
        ' for color palettes, we want to re-order entries when per-color alpha is used.
        ' Why the hassle? Shrink PNG a bit more. When color palettes have transparency,
        ' you must have a 1 byte Alpha value for each palette entry. But, that 1 byte
        ' alpha value, when = 255, is optional and implied. Therefore, if we move all
        ' palette entries with transparency to top of array, then all those 255s at the
        ' bottom of the array don't need to be cached in the PNG; not being there, PNG
        ' decoders must assume value is 255. We can save anywhere up to 200+ bytes
        ' depending on the image.
        ReDim palXRef(0 To 1, 0 To palCount - 1)
        
        If m_Trans = -1& Then   ' no transparencies and not grayscale
            Erase m_transPal
            For x = 0& To palCount - 1& ' all entries are opaque, no cross-referencing needed
                palXRef(0, x) = x
                palXRef(1, x) = x
            Next
        Else                    ' per-color alpha being used
            ' since we are re-ordering, we also need to build a cross-reference so
            ' we can reference palette locations, old to new and vice versa
            y = 0&: x = palCount - 1&  ' starting points for top & bottom of array
            For Index = 0& To palCount - 1&
                If m_transPal(Index + 1&) = 255 Then
                    palXRef(1, x) = Index ' keep full opaque entries at bottom of array
                    palXRef(0, Index) = x ' double link reference
                    x = x - 1&
                Else
                    palXRef(0, Index) = y ' move non-opaque entries near top of array
                    palXRef(1, y) = Index ' double link reference
                    y = y + 1&
                End If
            Next
        End If
        
        ' now we build our 8 bpp paletted image, referencing the re-sorted palette entires
        For y = 0& To UBound(m_Stream, 2)
            Index = y * scanWidth
            For x = 0& To UBound(m_Stream, 1) Step 4&
                ' get 32bit color from DIB
                CopyMemory Color, m_Stream(x, y), 4&
                ' locate it in our temp palette using binary search algorithm
                Color = FindColor(tSortIndex, Color, palCount, False)
                ' now cache its re-sorted reference
                m_Uncompressed(Index) = palXRef(0, Color - 1&)
                Index = Index + 1&
            Next
        Next
        
        ' good, now we need to build the palette the PNG will use,
        ' but we will be using 3 byte values, not 4 byte values & colors need to be RGB vs BGR
        ReDim m_Palette(1 To palCount * 3& + 4&) ' extra 4 bytes are used during Write_PLTE
        For x = 1& To palCount
            ' calculate new index for this palette entry
            Index = palXRef(0, x - 1&) * 3& + 5&   ' offset that extra 4 bytes too
            ' simultaneously remove pre-multiplication
            Select Case m_transPal(x)
            Case 255    ' full opaque
                m_Palette(Index) = (tPalette(x) \ &H10000) And &HFF&
                m_Palette(Index + 1&) = (tPalette(x) \ &H100&) And &HFF&
                m_Palette(Index + 2&) = tPalette(x) And &HFF&
            Case 0: ' do nothing, color is always 0,0,0
            Case Else
                m_Palette(Index) = (((tPalette(x) \ &H10000) And &HFF&) * m_transPal(x) \ 255)
                m_Palette(Index + 1&) = (((tPalette(x) \ &H100&) And &HFF&) * m_transPal(x) \ 255)
                m_Palette(Index + 2&) = ((tPalette(x) And &HFF&) * m_transPal(x) \ 255)
            End Select
        Next
        Erase tPalette()
        
        If Not m_Trans = -1& Then
            ' now we are going to double check how many non-opaque palette entries we have
            For x = 0& To palCount - 1&
                If m_transPal(palXRef(1, x) + 1&) = 255 Then
                    palCount = x    ' we saved 256-X bytes at least
                    Exit For
                End If
            Next
            ReDim palAlpha(1 To palCount + 4&)   ' extra 4 bytes used in Write_tRNS
            ' rewrite the m_transPal array, only caching non-opaque palette entries
            For x = 0& To palCount - 1&
                palAlpha(x + 5&) = m_transPal(palXRef(1, x) + 1&)
            Next
            m_transPal = palAlpha
        
        End If
    End If
    
    PalettizeImage = True

ExitRoutine:
End Function

Private Function FindColor(ByRef PaletteItems() As Long, ByVal Color As Long, ByVal Count As Long, ByRef isNew As Boolean) As Long

    ' MODIFIED BINARY SEARCH ALGORITHM -- Divide and conquer.
    ' Binary search algorithms are about the fastest on the planet, but
    ' its biggest disadvantage is that the array must already be sorted.
    ' Ex: binary search can find a value among 1 million values in less than 20 iterations
    
    ' [in] PaletteItems(). Long Array to search within. Array must be 1-bound
    ' [in] Color. A value to search for. Order is always ascending
    ' [in] Count. Number of items in PaletteItems() to compare against
    ' [out] isNew. If Color not found, isNew is True else False
    ' [out] Return value: The Index where Color was found or where the new Color should be inserted

    Dim UB As Long, LB As Long
    Dim newIndex As Long
    
    If Count = 0& Then
        FindColor = 1&
        isNew = True
        Exit Function
    End If
    
    UB = Count
    LB = 1&
    
    Do Until LB > UB
        newIndex = LB + ((UB - LB) \ 2&)
        If PaletteItems(newIndex) = Color Then
            Exit Do
        ElseIf PaletteItems(newIndex) > Color Then ' new color is lower in sort order
            UB = newIndex - 1&
        Else ' new color is higher in sort order
            LB = newIndex + 1&
        End If
    Loop

    If LB > UB Then  ' color was not found
            
        If Color > PaletteItems(newIndex) Then newIndex = newIndex + 1&
        isNew = True
        
    Else
        isNew = False
    End If
    
    FindColor = newIndex

End Function


Private Function ByteAlignOnByte(ByVal totalWidth As Long, ByVal btsPerPixel As Byte) As Long
' // LaVolpe, Dec 1 thru 10
    ' returns number of bytes required to display n pixels at p color depth (byte aligned)
    ByteAlignOnByte = (totalWidth * btsPerPixel + 7&) \ 8&
End Function

Private Function Write_IHDR(fileNum As Long, Stream() As Byte, Host As c32bppDIB, isInterlaced As Boolean) As Boolean

    Const png_Signature1 As Long = 1196314761
    Const png_Signature2 As Long = 169478669
    Const chnk_IHDR As Long = &H52444849 'Image header
    
    On Error GoTo eh
    Dim pngData(0 To 16) As Byte ' 13 byte header + 4 byte chunk name
    Dim gpLong As Long           ' general purpose variable
    Dim rwLen As Long
    
    ' build header
    CopyMemory pngData(0), chnk_IHDR, 4&    ' chunk name
    gpLong = iparseReverseLong(Host.Width)   ' png width
    CopyMemory pngData(4), gpLong, 4&
    gpLong = iparseReverseLong(Host.Height)  ' png height
    CopyMemory pngData(8), gpLong, 4&
    
    ' bit depth, 16bit (PNG 16 bytes per R,G,B element or 48 bytes per pixel)
    ' not supported via this class
    pngData(12) = 8 ' only 1,2,4,48 bpp are different, 8,24,32 bpp is 8
    
    pngData(13) = m_ColorType
    ' pngData(14) & (15) will always be zero (compression/filter methods)
    ' next byte is 1 if interlaced
    pngData(16) = Abs(isInterlaced)

    If fileNum = 0& Then ' writing to array vs file
        ReDim Stream(0 To 32) ' png signature, header len, header, crc value (33 bytes)
        CopyMemory Stream(0), png_Signature1, 4&
        CopyMemory Stream(4), png_Signature2, 4&
        gpLong = iparseReverseLong(13&) ' len of header
        CopyMemory Stream(8), gpLong, 4&
        CopyMemory Stream(12), pngData(0), 17&
        gpLong = zCreateCRC(VarPtr(pngData(0)), 17&)
        CopyMemory Stream(29), gpLong, 4&
        Write_IHDR = True
    Else
    
        WriteFile fileNum, png_Signature1, 4&, rwLen, ByVal 0&
        If rwLen = 4& Then
            WriteFile fileNum, png_Signature2, rwLen, rwLen, ByVal 0&
            If rwLen = 4& Then
                WriteFile fileNum, iparseReverseLong(13&), rwLen, rwLen, ByVal 0&
                If rwLen = 4& Then
                    WriteFile fileNum, pngData(0), 17&, rwLen, ByVal 0&
                    If rwLen = 17& Then
                        WriteFile fileNum, zCreateCRC(VarPtr(pngData(0)), rwLen), 4&, rwLen, ByVal 0&
                        Write_IHDR = (rwLen = 4&)
                    End If
                End If
            End If
        End If
    End If
eh:
    If Err Then Err.Clear
End Function


Private Function Write_PLTE(fileNum As Long, Stream() As Byte, Invalid_bKGD As Boolean) As Boolean

    ' Note: the palette is preprocessed before it arrives here: BGR>RGB
    On Error GoTo eh
    
    If m_ColorType = clrPalette Then ' paletted images only
    
        Const chnk_PLTE As Long = &H45544C50 'Palette
        
        Dim gpLong As Long          ' general purpose variable
        Dim Index As Long
        Dim rwLen As Long
        
        ' when paletted, the bKGD chunk comes after the palette, but for palettes the
        ' bkgd chunk must be one of the palette entries, therefore, we will attempt to
        ' find the color in the palette, add it to the palette if possible, or skip
        ' the optional chunk if color is not in the palette
        If (m_PNGprops And ePngProperties.colorDefaultBkg) = ePngProperties.colorDefaultBkg Then
            Dim bkg(0 To 2) As Byte
            CopyMemory bkg(0), m_bKGD, 3&
            For Index = 5& To UBound(m_Palette) Step 3&
                If bkg(0) = m_Palette(Index) Then
                    If bkg(1) = m_Palette(Index + 1&) Then
                        If bkg(2) = m_Palette(Index + 2&) Then Exit For
                    End If
                End If
            Next
            If Index < UBound(m_Palette) Then   ' found it, ref the index
                m_bKGD = (Index - 5&) \ 3&
            ElseIf UBound(m_Palette) < 772& Then ' we can add it, let's do that
                ' ^^ 772 is 256*3+4
                ReDim Preserve m_Palette(1 To UBound(m_Palette) + 3&)
                m_bKGD = (UBound(m_Palette) - 5&) \ 3&
                CopyMemory m_Palette(UBound(m_Palette) - 2&), bkg(0), 3&
            Else
                Invalid_bKGD = True ' do not write the bkgd chunk
            End If
        End If
            
        CopyMemory m_Palette(1), chnk_PLTE, 4&
        gpLong = UBound(m_Palette)
        
        If fileNum = 0& Then 'writing to array vs file
            Index = UBound(Stream) + 1&
            ReDim Preserve Stream(0 To Index + gpLong + 7&)
            rwLen = iparseReverseLong(gpLong - 4&)
            CopyMemory Stream(Index), rwLen, 4&                 ' size of chunk
            CopyMemory Stream(Index + 4&), m_Palette(1), gpLong ' palette
            rwLen = zCreateCRC(VarPtr(m_Palette(1)), gpLong)
            CopyMemory Stream(Index + gpLong + 4&), rwLen, 4&   ' crc
            Write_PLTE = True
        Else
            WriteFile fileNum, iparseReverseLong(gpLong - 4&), 4&, rwLen, ByVal 0&
            If rwLen = 4& Then
                WriteFile fileNum, m_Palette(1), gpLong, rwLen, ByVal 0&
                If rwLen = gpLong Then
                    WriteFile fileNum, zCreateCRC(VarPtr(m_Palette(1)), gpLong), 4&, rwLen, ByVal 0&
                    Write_PLTE = (rwLen = 4&)
                End If
            End If
        End If
        Erase m_Palette()   ' no longer needed
    Else
        Write_PLTE = True
    End If
eh:
    If Err Then Err.Clear

End Function

Private Function Write_tEXt(fileNum As Long, Stream() As Byte, bTitleAuthorOnly As Boolean) As Boolean

    ' Function writes uncompressed standard Keywords & text to the PNG
    
    ' Note. Per PNG specs, some text should be written near top of the file while others
    ' should be written near the end. There is no requirement for text to appear in
    ' any specific location. The logic for writing some near the top is for search
    ' engines only. It would be faster to find that text if nearer the top.
    ' Therefore, this routine is called twice, once near the top of the PNG and
    ' and again just before the IEND chunk is written
    
    Const chnk_tEXt As Long = &H74584574 'Text - uncompressed
    
    On Error GoTo ExitRoutine
    
    Dim pngData() As Byte   ' data to be written to PNG file
    Dim txtData() As Byte
    Dim gpLong As Long
    Dim lenKeyword As Long
    Dim lenText As Long
    
    Dim Index As Long
    Dim CaptionID As Long
    Dim tProps As Long
    Dim lastCaption As Long
    Dim keyWord As String
    Dim rwLen As Long
    
    If bTitleAuthorOnly Then    ' called after writing IHDR
        CaptionID = ePngProperties.txtTitle
        lastCaption = ePngProperties.txtDescription
    Else                        ' called before writing IEND
        CaptionID = ePngProperties.txtDescription
        lastCaption = ePngProperties.txtLargeBlockText
    End If
    tProps = m_PNGprops
    Do Until CaptionID = lastCaption
        If (tProps And CaptionID) = CaptionID Then
            tProps = tProps And Not CaptionID
            Select Case CaptionID
            Case txtTitle: Index = 0
                keyWord = "Title" & Chr$(0)
            Case txtAuthor: Index = 1&
                keyWord = "Author" & Chr$(0)
            Case txtComment: Index = 9&
                keyWord = "Comment" & Chr$(0)
            Case txtCopyright: Index = 3&
                keyWord = "Copyright" & Chr$(0)
            Case txtCreationTime: Index = 4&
                keyWord = "Creation Time" & Chr$(0)
            Case txtDescription: Index = 2&
                keyWord = "Description" & Chr$(0)
            Case txtDisclaimer: Index = 6&
                keyWord = "Disclaimer" & Chr$(0)
            Case txtSoftware: Index = 5&
                keyWord = "Software" & Chr$(0)
            Case txtSource: Index = 8&
                keyWord = "Source" & Chr$(0)
            Case txtWarning: Index = 7&
                keyWord = "Warning" & Chr$(0)
            End Select
                
            ' tXTt chunk format::
            'Keyword 1-79 bytes (character string)
            'Null separator 1 byte (null character)
            'Text string 0 or more bytes (character string)
            
            lenKeyword = Len(keyWord)
            txtData() = StrConv(keyWord, vbFromUnicode)
            If Len(m_Captions(Index)) > 0& Then
                lenText = Len(m_Captions(Index))
                ReDim pngData(1 To lenKeyword + lenText + 4&)
                CopyMemory pngData(5), txtData(0), lenKeyword
                txtData() = StrConv(m_Captions(Index), vbFromUnicode)
                CopyMemory pngData(5& + lenKeyword), txtData(0), lenText
                
            Else ' handle zero-length chunks.
                ' Note: I would prefer to just skip these, but maybe you might
                ' decide to use one as a flag for something else?
                ReDim pngData(1 To lenKeyword + 4&)
                CopyMemory pngData(5), txtData(0), lenKeyword
            End If
            CopyMemory pngData(1), chnk_tEXt, 4&
            gpLong = lenKeyword + lenText + 4&
            
            If fileNum = 0& Then ' writing to stream
                Index = UBound(Stream) + 1&
                ReDim Preserve Stream(0 To Index + gpLong + 7&)
                rwLen = iparseReverseLong(gpLong - 4&)
                CopyMemory Stream(Index), rwLen, 4&
                CopyMemory Stream(Index + 4), pngData(1), gpLong
                rwLen = zCreateCRC(VarPtr(pngData(1)), gpLong)
                CopyMemory Stream(Index + 4& + gpLong), rwLen, 4&
                Write_tEXt = True
            Else
                WriteFile fileNum, iparseReverseLong(gpLong - 4&), 4&, rwLen, ByVal 0&
                If rwLen = 4& Then
                    WriteFile fileNum, pngData(1), gpLong, rwLen, ByVal 0&
                    If rwLen = gpLong Then
                        WriteFile fileNum, zCreateCRC(VarPtr(pngData(1)), gpLong), 4&, rwLen, ByVal 0&
                        Write_tEXt = (rwLen = 4&)
                    End If
                End If
            End If
        End If
        CaptionID = CaptionID * 2&
    Loop
    
ExitRoutine:
    If Err Then
        Err.Clear
    Else
        If lenKeyword = 0& Then Write_tEXt = True
    End If

End Function

Private Function Write_tIMe(fileNum As Long, Stream() As Byte) As Boolean

    ' Note: the time stamp should be Universal Time, not local area
    
    If (m_PNGprops And ePngProperties.dateTimeModified) = ePngProperties.dateTimeModified Then

        Const chnk_tIME As Long = &H454D4974 'Timestamp
        
        On Error GoTo eh
        Dim pngData(0 To 10) As Byte ' 7 byte date/time + 4 byte chunk name
        Dim gpLong As Long
        Dim gpInt As Integer
        Dim dtStamp As Date
        Dim rwLen As Long
        
        dtStamp = CDate(m_Captions(10))
        
        CopyMemory pngData(0), chnk_tIME, 4&
            gpInt = Year(dtStamp)
        CopyMemory pngData(5), gpInt, 2&
        pngData(4) = pngData(6)             ' swap endian of integer
            gpInt = Month(dtStamp)
        CopyMemory pngData(6), gpInt, 1&
            gpInt = Day(dtStamp)
        CopyMemory pngData(7), gpInt, 1&
            gpInt = Hour(dtStamp)
        CopyMemory pngData(8), gpInt, 1&
            gpInt = Minute(dtStamp)
        CopyMemory pngData(9), gpInt, 1&
            gpInt = Second(dtStamp)
        CopyMemory pngData(10), gpInt, 1&
        
        If fileNum = 0& Then ' writing to stream
            gpLong = UBound(Stream) + 1&
            ReDim Preserve Stream(0 To gpLong + 18&)
            rwLen = iparseReverseLong(7)
            CopyMemory Stream(gpLong), rwLen, 4&
            CopyMemory Stream(gpLong + 4&), pngData(0), 11&
            rwLen = zCreateCRC(VarPtr(pngData(0)), 11&)
            CopyMemory Stream(gpLong + 15&), rwLen, 4&
            Write_tIMe = True
        Else
            WriteFile fileNum, iparseReverseLong(7), 4&, rwLen, ByVal 0&
            If rwLen = 4& Then
                WriteFile fileNum, pngData(0), 11&, rwLen, ByVal 0&
                If rwLen = 11& Then
                    WriteFile fileNum, zCreateCRC(VarPtr(pngData(0)), rwLen), 4&, rwLen, ByVal 0&
                    Write_tIMe = (rwLen = 4&)
                End If
            End If
        End If
    Else
        Write_tIMe = True
    End If
eh:
    If Err Then Err.Clear

End Function

Private Function Write_tRNS(fileNum As Long, Stream() As Byte) As Boolean

    ' For paletted/grayscale images, tRNS is the palette index, otherwise RGB value
    On Error GoTo eh
        
    If m_Trans = -1& Then
        Write_tRNS = True
    
    Else ' transparency not used
    
        Const chnk_tRNS As Long = &H534E5274 'Simple Transparency & palette transparency
        Dim Index As Long
        Dim gpLong As Long
        Dim rwLen As Long
    
        Select Case m_ColorType
            
            Case clrPalette ' Paletted (palette count * 3 + 4 byte chunk name)
                ' nothing to do; done during PalettizeImage
            
            Case clrGrayScale ' grayscale
                ReDim m_transPal(1 To 6)   ' 2 bytes + 4 byte chunk name
                m_transPal(6) = m_Trans
                ' Note: m_transPal(5) used with 48bit per pixel images (not supported)
            
            Case clrTrueColor ' we have simple transparency for true color
                ReDim m_transPal(1 To 10)   ' 6 bytes + 4 byte chunk name
                m_transPal(6) = m_Trans And &HFF
                m_transPal(8) = (m_Trans \ &H100&) And &HFF
                m_transPal(10) = (m_Trans \ &H10000) And &HFF
                ' Note: m_transPal(5,7,9) used with 48bit per pixel images (not supported)
        
            Case Else
                ' Color Types 4 & 6 are prohibited from having a tRNS chunk
                Write_tRNS = True
                Exit Function
        End Select
        
        CopyMemory m_transPal(1), chnk_tRNS, 4&
        gpLong = UBound(m_transPal)
    
        ' write the chunk
        If fileNum = 0& Then ' writing to array vs file
            Index = UBound(Stream) + 1&
            ReDim Preserve Stream(0 To Index + gpLong + 7&)
            rwLen = iparseReverseLong(gpLong - 4&)
            CopyMemory Stream(Index), rwLen, 4&                 ' chunk size
            CopyMemory Stream(Index + 4&), m_transPal(1), gpLong ' palette
            gpLong = zCreateCRC(VarPtr(m_transPal(1)), gpLong)
            CopyMemory Stream(Index + UBound(m_transPal) + 4&), gpLong, 4&       ' crc value
            Write_tRNS = True
        Else
            WriteFile fileNum, iparseReverseLong(gpLong - 4&), 4&, rwLen, ByVal 0&
            If rwLen = 4& Then
                WriteFile fileNum, m_transPal(1), gpLong, rwLen, ByVal 0&
                If rwLen = gpLong Then
                    WriteFile fileNum, zCreateCRC(VarPtr(m_transPal(1)), gpLong), 4&, rwLen, ByVal 0&
                    Write_tRNS = (rwLen = 4&)
                End If
            End If
        End If
        Erase m_transPal()
    End If
eh:
    If Err Then Err.Clear

End Function

Private Function Write_zTXt(fileNum As Long, Stream() As Byte) As Boolean

    ' Function writes non-reserved keyword compressed/uncompressed text to the PNG
    
    If (m_PNGprops And ePngProperties.txtLargeBlockText) = ePngProperties.txtLargeBlockText Then

        On Error GoTo eh

        Const chnk_tEXt As Long = &H74584574 'Text - uncompressed
        Const chnk_zTXt As Long = &H7458547A 'Text - compressed
        
        Dim txtData() As Byte   ' comments/text in bytes
        Dim pngData() As Byte   ' data to be written to PNG file
        Dim sText As String
        Dim gpLong As Long
        Dim Index As Long
        Dim rwLen As Long
        Dim lenKeyword As Long
        Dim lenText As Long
        Dim bWritten As Boolean
    
        For Index = 11& To UBound(m_Captions)
            ' convert keyword to bytes
            lenKeyword = InStr(m_Captions(Index), Chr$(0))
            lenText = Len(m_Captions(Index)) - lenKeyword
            txtData() = StrConv(m_Captions(Index), vbFromUnicode)
            
            ' per PNG specs....
            ' It is recommended that text items less than 1K (1024 bytes)
            ' in size should be output using uncompressed text chunks
            If lenText > 1024& Then
            
                ' IMPORTANT: This portion of the routine is not equipped to write
                ' zero-length text block. That is only handled above where the
                ' .Text length is < 1025... DO NOT modify that IF statement to
                ' allow zero-length chunks to fall thru to this portion of IF
                
                ' zTXt chunk format::
                'Keyword 1-79 bytes (character string)
                'Null separator 1 byte (null character)
                'Compression method 1 byte
                'Compressed text datastream n bytes
                
                ' Note that the compression byte of zero needs to be included too,
                ' but we don't add it to the txtData conversion above cause zero
                ' would be converted to 48 -- Asc("0").
                
                gpLong = lenText * 0.01 + 12& + lenText
                '^^ Text won't always compress smaller; it should, but may not
                ' That is why it is recommended to allow 1024 bytes as uncompressed
                ReDim pngData(1 To gpLong + (lenKeyword + 5&))
                ' ^^ include 4 bytes for chunk name + keyword length + 1 byte compression method
                    
                If zDeflate(VarPtr(pngData(6& + lenKeyword)), gpLong, VarPtr(txtData(lenKeyword)), lenText) = True Then
                    ' ^^ store compression after chunk name, after keyword and after compression method
                    ' ^^ begin compression on 1st byte of the text, not the caption or compression method
                    
                    CopyMemory pngData(1), chnk_zTXt, 4&
                    CopyMemory pngData(5), txtData(0), lenKeyword
                    gpLong = gpLong + lenKeyword + 5&

                    If fileNum = 0& Then    ' writing to array
                        Index = UBound(Stream) + 1&
                        ReDim Preserve Stream(0 To Index + gpLong + 7&)
                        rwLen = iparseReverseLong(gpLong - 4&)
                        CopyMemory Stream(Index), rwLen, 4&
                        CopyMemory Stream(Index + 4&), pngData(1), gpLong
                        rwLen = zCreateCRC(VarPtr(pngData(1)), gpLong)
                        CopyMemory Stream(Index + gpLong + 4&), rwLen, 4&
                        Write_zTXt = True
                    Else
                        WriteFile fileNum, iparseReverseLong(gpLong - 4&), 4&, rwLen, ByVal 0&
                        If rwLen = 4& Then
                            WriteFile fileNum, pngData(1), gpLong, rwLen, ByVal 0&
                            If rwLen = gpLong Then
                                WriteFile fileNum, zCreateCRC(VarPtr(pngData(1)), gpLong), 4&, rwLen, ByVal 0&
                                Write_zTXt = (rwLen = 4&)
                            End If
                        End If
                    End If
                    bWritten = True
                Else    ' failed to compress. Which means our buffer was too small
                        ' Therefore we will add it as uncompressed instead of
                        ' making the buffer even bigger
                End If
            End If
            
            If Not bWritten Then 'either len<1025 or compression failed
                ' tXTt chunk format::
                'Keyword 1-79 bytes (character string)
                'Null separator 1 byte (null character)
                'Text string 0 or more bytes (character string)

                gpLong = lenText + lenKeyword + 4&  ' size of chunk
                ReDim pngData(1 To gpLong)
                CopyMemory pngData(1), chnk_tEXt, 4&
                CopyMemory pngData(5), txtData(0), lenKeyword
                
                If Not lenText = 0& Then ' zero-length text; not prohibited by PNG specs
                    CopyMemory pngData(5 + lenKeyword), txtData(lenKeyword), lenText
                End If
                
                If fileNum = 0& Then ' writing to array
                    Index = UBound(Stream) + 1&
                    ReDim Preserve Stream(0 To Index + gpLong + 7&)
                    rwLen = iparseReverseLong(gpLong - 4&)
                    CopyMemory Stream(Index), rwLen, 4&
                    CopyMemory Stream(Index + 4&), pngData(1), gpLong
                    rwLen = zCreateCRC(VarPtr(pngData(1)), gpLong)
                    CopyMemory Stream(Index + gpLong + 4&), rwLen, 4&
                    Write_zTXt = True
                Else
                    WriteFile fileNum, iparseReverseLong(gpLong - 4&), 4&, rwLen, ByVal 0&
                    If rwLen = 4& Then
                        WriteFile fileNum, pngData(1), gpLong, rwLen, ByVal 0&
                        If rwLen = gpLong Then
                            WriteFile fileNum, zCreateCRC(VarPtr(pngData(1)), gpLong), 4&, rwLen, ByVal 0&
                            Write_zTXt = (rwLen = 4&)
                        End If
                    End If
                End If
            End If
        Next
    Else
        Write_zTXt = True
    End If
    
eh:
    If Err Then Err.Clear

End Function

Private Function Write_bKGD(fileNum As Long, Stream() As Byte) As Boolean

    ' For paletted/grayscale images, this is the palette index, otherwise RGB value
    On Error GoTo eh
    Const chnk_bKGD As Long = &H44474B62 'Window Background Color
    
    If (m_PNGprops And ePngProperties.colorDefaultBkg) = ePngProperties.colorDefaultBkg Then
    
        Dim pngData() As Byte
        Dim gpLong As Long
        Dim rwLen As Long
        Dim Index As Long
            
        ' Per PNG specs, bKGD chunk must come before IDAT and after PLTE
        Select Case m_ColorType
        Case clrPalette  ' 1 byte + 4 byte chunk name
            ReDim pngData(0 To 4)
            pngData(4) = CByte(m_bKGD)
        
        Case clrGrayScale, clrGrayAlpha   ' grayscales, 2 bytes + 4 byte chunk name
            ReDim pngData(0 To 6)
            pngData(5) = (m_bKGD And &HFF)
            ' pngData(4) used with 48bit per pixel images (not supported)
            
        Case Else ' true color, RGB format
            ReDim pngData(0 To 9)   ' 6 bytes + 4 byte chunk name
            pngData(5) = m_bKGD And &HFF
            pngData(7) = (m_bKGD \ &H100&) And &HFF
            pngData(9) = (m_bKGD \ &H10000) And &HFF
            ' Note: pngData(4,6,8) used with 48bit per pixel images (not supported)
        End Select
        
        CopyMemory pngData(0), chnk_bKGD, 4&
        gpLong = UBound(pngData) + 1&
        
        If fileNum = 0& Then 'writing to array
            Index = UBound(Stream) + 1&
            ReDim Preserve Stream(0 To Index + gpLong + 7&)
            rwLen = iparseReverseLong(gpLong - 4&)
            CopyMemory Stream(Index), rwLen, 4&
            CopyMemory Stream(Index + 4&), pngData(0), gpLong
            rwLen = zCreateCRC(VarPtr(pngData(0)), gpLong)
            CopyMemory Stream(Index + gpLong + 4&), rwLen, 4&
            Write_bKGD = True
        Else
            WriteFile fileNum, iparseReverseLong(gpLong - 4&), 4&, rwLen, ByVal 0&
            If rwLen = 4& Then
                WriteFile fileNum, pngData(0), gpLong, rwLen, ByVal 0&
                If rwLen = gpLong Then
                    WriteFile fileNum, zCreateCRC(VarPtr(pngData(0)), gpLong), 4&, rwLen, ByVal 0&
                    Write_bKGD = (rwLen = 4&)
                End If
            End If
        End If
    Else
        Write_bKGD = True
    End If
eh:
    If Err Then Err.Clear
        
End Function

Private Function Write_IDAT(fileNum As Long, outStream() As Byte, imgData() As Byte, filterLen As Long) As Boolean

    ' Function writes the IDAT chunk(s). If more than one, they must be back to back
    ' Note: IDATs can be written in multiple chunks; if so, chunks must be consecutive

    Const chnk_IDAT As Long = &H54414449 'Image data
    
    On Error GoTo eh
    Dim gpLong As Long, Index As Long
    Dim rwLen As Long
    
    CopyMemory imgData(0), chnk_IDAT, 4&
    If fileNum = 0& Then ' writing to array vs file
        Index = UBound(outStream) + 1&
        ReDim Preserve outStream(0 To Index + filterLen + 11&)
        gpLong = iparseReverseLong(filterLen)
        CopyMemory outStream(Index), gpLong, 4&  ' add chunk size
        CopyMemory outStream(Index + 4&), imgData(0), filterLen + 4& ' add compressed data
        gpLong = zCreateCRC(VarPtr(imgData(0)), filterLen + 4&)
        CopyMemory outStream(Index + 8& + filterLen), gpLong, 4& ' add crc value
        Write_IDAT = True
    Else
        
        WriteFile fileNum, iparseReverseLong(filterLen), 4&, rwLen, ByVal 0&
        If rwLen = 4& Then
            WriteFile fileNum, imgData(0), filterLen + 4&, rwLen, ByVal 0&
            If rwLen = filterLen + 4& Then
                WriteFile fileNum, zCreateCRC(VarPtr(imgData(0)), rwLen), 4&, rwLen, ByVal 0&
                Write_IDAT = (rwLen = 4&)
            End If
        End If
    End If
eh:
    If Err Then Err.Clear

End Function

Private Function Write_IEND(fileNum As Long, Stream() As Byte) As Boolean

    Const chnk_IEND As Long = &H444E4549 'End of Image
    
    On Error GoTo eh
    Dim Index As Long
    Dim gpLong As Long
    Dim rwLen As Long
    
    If fileNum = 0 Then ' writing to array vs file
    
        Index = UBound(Stream) + 1&
        ReDim Preserve Stream(0 To Index + 11&)
        CopyMemory Stream(Index), 0&, 4&
        CopyMemory Stream(Index + 4), chnk_IEND, 4&   ' chunk name, chunk length is zero
        gpLong = zCreateCRC(VarPtr(chnk_IEND), 4&)
        CopyMemory Stream(Index + 8&), gpLong, 4&    ' crc value
        Write_IEND = True
    Else
    
        WriteFile fileNum, rwLen, 4&, rwLen, ByVal 0&
        If rwLen = 4& Then
            WriteFile fileNum, chnk_IEND, 4&, rwLen, ByVal 0&
            If rwLen = 4& Then
                WriteFile fileNum, zCreateCRC(VarPtr(chnk_IEND), 4&), 4&, rwLen, ByVal 0&
                Write_IEND = (rwLen = 4&)
            End If
        End If
    End If
eh:
    If Err Then Err.Clear

End Function

Private Sub EncodeFilter_None(pngData() As Byte, _
                        ByVal RowNr As Long, dibRowNr As Long, _
                        ByVal scanLineDIB As Long, scanLinePNG As Long, _
                        stepVal As Byte, AdptValue As Long)

    ' this routine is only called when adapative filter method is used
    
    Dim x As Long
    Dim startByte As Long, locDIB As Long
    Dim lTest As Long
    
    If scanLineDIB > -1 Then ' processing interlaced image
        ' for interlaced, m_Uncompressed will be a top-down calculated array
        ' and the scanLineDIB parameter is an offset into the interlaced array
        startByte = scanLineDIB + 1
        locDIB = startByte
    Else
        ' for non-interlaced, m_Uncompressed will be a bottom-up DIB
        locDIB = dibRowNr * -scanLineDIB
        startByte = RowNr * scanLinePNG + RowNr + 1
   End If
    
    For x = locDIB To locDIB + scanLinePNG - 1
        lTest = lTest + m_Uncompressed(x)
        If lTest > AdptValue Then Exit Sub
    Next
    
    If lTest = 0 Then lTest = 1
    AdptValue = lTest
    pngData(startByte - 1) = 0

End Sub


Private Sub EncodeFilter_Up(pngData() As Byte, _
                        ByVal RowNr As Long, dibRowNr As Long, _
                        ByVal scanLineDIB As Long, scanLinePNG As Long, _
                        stepVal As Byte, AdptValue As Long)

' this is Filter Type 2
'http://www.w3.org/TR/PNG/#9-table91
    
    Dim ppTop As Integer
    Dim x As Long
    Dim startByte As Long, locDIB As Long
    Dim lTest As Long, prevRow As Long
    
    If scanLineDIB > -1 Then ' processing interlaced image
        ' for interlaced, m_Uncompressed will be a top-down calculated array
        ' and the scanLineDIB parameter is an offset into the interlaced array
        startByte = scanLineDIB + 1
        scanLineDIB = scanLinePNG + 1
        locDIB = startByte
    Else
        ' for non-interlaced, m_Uncompressed will be a bottom-up DIB
        locDIB = dibRowNr * -scanLineDIB
        startByte = RowNr * scanLinePNG + RowNr + 1
    End If
        
    If AdptValue Then
        
        If RowNr Then
            For x = locDIB To locDIB + scanLinePNG - 1
                lTest = lTest + Abs(0 + m_Uncompressed(x) - m_Uncompressed(x - scanLineDIB))
                If lTest > AdptValue Then Exit Sub
            Next
            
            If lTest = 0 Then lTest = 1
            AdptValue = lTest
            pngData(startByte - 1) = 2
        End If
        
    Else
        For x = 0 To scanLinePNG - 1
            If RowNr Then ppTop = m_Uncompressed(locDIB + x - scanLineDIB)
            ' VB workaround for C++ unsigned math
            If ppTop > m_Uncompressed(locDIB + x) Then
                pngData(startByte + x) = 256 - ppTop + m_Uncompressed(locDIB + x)
            Else
                pngData(startByte + x) = m_Uncompressed(locDIB + x) - ppTop
            End If
        Next
        pngData(startByte - 1) = 2
    End If

End Sub

Private Sub EncodeFilter_Sub(pngData() As Byte, _
                        ByVal RowNr As Long, dibRowNr As Long, _
                        ByVal scanLineDIB As Long, scanLinePNG As Long, _
                        stepVal As Byte, AdptValue As Long)

' This is Filter Type 1
'http://www.w3.org/TR/PNG/#9-table91

    Dim x As Long
    Dim startByte As Long, locDIB As Long
    Dim lTest As Long
    
    If scanLineDIB > -1 Then ' processing interlaced image
        ' for interlaced, m_Uncompressed will be a top-down calculated array
        ' and the scanLineDIB parameter is an offset into the interlaced array
        startByte = scanLineDIB + 1
        scanLineDIB = scanLinePNG + 1
        locDIB = startByte
    Else
        ' for non-interlaced, m_Uncompressed will be a bottom-up DIB
        locDIB = dibRowNr * -scanLineDIB
        startByte = RowNr * scanLinePNG + RowNr + 1
    End If
        
    If AdptValue Then
    
        ' 1st n bytes for 1st pixel are unfiltered
        For x = locDIB To stepVal + locDIB - 1
            lTest = lTest + m_Uncompressed(x)
        Next
        
        For x = locDIB + stepVal To scanLinePNG - 1
            lTest = lTest + Abs(0 + m_Uncompressed(x) - m_Uncompressed(x - stepVal))
            If lTest > AdptValue Then Exit Sub
        Next
        
        If lTest = 0 Then lTest = 1
        AdptValue = lTest
        
    Else
        ' 1st n bytes for 1st pixel are unfiltered
        CopyMemory pngData(startByte), m_Uncompressed(locDIB), stepVal
        
        For x = stepVal To scanLinePNG - 1
            ' VB workaround for C++ unsigned math
            If m_Uncompressed(locDIB + x - stepVal) > m_Uncompressed(locDIB + x) Then
                pngData(startByte + x) = 256 - m_Uncompressed(locDIB + x - stepVal) + m_Uncompressed(locDIB + x)
            Else
                pngData(startByte + x) = m_Uncompressed(locDIB + x) - m_Uncompressed(locDIB + x - stepVal)
            End If
        Next
    End If
    pngData(startByte - 1) = 1

End Sub

Private Sub EncodeFilter_Avg(pngData() As Byte, _
                        ByVal RowNr As Long, dibRowNr As Long, _
                        ByVal scanLineDIB As Long, scanLinePNG As Long, _
                        stepVal As Byte, AdptValue As Long)


' This is Filter Type 3
'http://www.w3.org/TR/PNG/#9-table91

    Dim ppLeft As Integer, ppTop As Integer
    Dim x As Long, pReturn As Integer
    Dim locDIB As Long, startByte As Long
    Dim lTest As Long
    
    If scanLineDIB > -1 Then ' processing interlaced image
        ' for interlaced, m_Uncompressed will be a top-down calculated array
        ' and the scanLineDIB parameter is an offset into the interlaced array
        startByte = scanLineDIB + 1
        scanLineDIB = scanLinePNG + 1
        locDIB = startByte
    Else
        ' for non-interlaced, m_Uncompressed will be a bottom-up DIB
        locDIB = dibRowNr * -scanLineDIB
        startByte = RowNr * scanLinePNG + RowNr + 1
    End If
    
    If AdptValue Then
    
        If RowNr Then
        
            For x = locDIB To locDIB + scanLinePNG - 1
                ppTop = m_Uncompressed(x - scanLineDIB)
                If x >= locDIB + stepVal Then ppLeft = m_Uncompressed(x - stepVal)
                lTest = lTest + Abs((m_Uncompressed(x) - (ppLeft + ppTop) \ 2))
                If lTest > AdptValue Then Exit Sub
            Next
            If lTest = 0 Then lTest = 1
            AdptValue = lTest
            pngData(startByte - 1) = 3
            
        End If
        
    Else
    
        For x = 0 To scanLinePNG - 1
        
            If RowNr Then ppTop = m_Uncompressed(locDIB - scanLineDIB + x)
            If x >= stepVal Then ppLeft = m_Uncompressed(locDIB - stepVal + x)
            
            pReturn = (ppLeft + ppTop) \ 2
            ' VB workaround for C++ unsigned math
            If pReturn > m_Uncompressed(locDIB + x) Then
                pngData(x + startByte) = 256 - pReturn + m_Uncompressed(x + locDIB)
            Else
                pngData(x + startByte) = m_Uncompressed(x + locDIB) - pReturn
            End If
        
        Next
        pngData(startByte - 1) = 3
    
    End If
    
End Sub

Private Sub EncodeFilter_Paeth(pngData() As Byte, _
                        ByVal RowNr As Long, dibRowNr As Long, _
                        ByVal scanLineDIB As Long, scanLinePNG As Long, _
                        stepVal As Byte, AdptValue As Long)

' This is Filter Type 4
'http://www.w3.org/TR/PNG/#9-table91

    Dim ppLeft As Integer, ppTop As Integer, ppTopLeft As Integer
    Dim x As Long, pReturn As Integer
    Dim startByte As Long, locDIB As Long
    Dim lTest As Long, prevRow As Long
    
    If scanLineDIB > -1 Then ' processing interlaced image
        ' for interlaced, m_Uncompressed will be a top-down calculated array
        ' and the scanLineDIB parameter is an offset into the interlaced array
        startByte = scanLineDIB + 1
        scanLineDIB = scanLinePNG + 1
        locDIB = startByte
    Else
        ' for non-interlaced, m_Uncompressed will be a bottom-up DIB
        locDIB = dibRowNr * -scanLineDIB
        startByte = RowNr * scanLinePNG + RowNr + 1
    End If
        
    If AdptValue Then
    
        If RowNr Then
            
            For x = locDIB To locDIB + scanLinePNG - 1
                If x >= stepVal + locDIB Then ' we are not on the 1st pixel
                    ppLeft = m_Uncompressed(x - stepVal)
                    ppTopLeft = m_Uncompressed(x - scanLineDIB - stepVal)
                End If
                ppTop = m_Uncompressed(x - scanLineDIB)
                ' get the Paeth closest neighbor
                lTest = lTest + Abs((m_Uncompressed(x) - PaethPredictor(ppLeft, ppTop, ppTopLeft)))
                If lTest > AdptValue Then Exit Sub
            Next
            
            If lTest = 0 Then lTest = 1
            AdptValue = lTest
            pngData(startByte - 1) = 4
        End If
    Else
    
        For x = 0 To scanLinePNG - 1
            
            If x >= stepVal Then ' we are not on the 1st pixel
                ppLeft = m_Uncompressed(locDIB + x - stepVal)
                If RowNr Then
                    prevRow = locDIB + x - scanLineDIB
                    ppTop = m_Uncompressed(prevRow)
                    ppTopLeft = m_Uncompressed(prevRow - stepVal)
                End If
            Else
                If RowNr Then ppTop = m_Uncompressed(locDIB + x - scanLineDIB)
            End If
            ' get the Paeth closest neighbor
            pReturn = PaethPredictor(ppLeft, ppTop, ppTopLeft)
            
            ' VB workaround for C++ unsigned math
            If pReturn > m_Uncompressed(locDIB + x) Then
                pngData(startByte + x) = 256 - pReturn + m_Uncompressed(locDIB + x)
            Else
                pngData(startByte + x) = m_Uncompressed(locDIB + x) - pReturn
            End If
            
        Next
        pngData(startByte - 1) = 4
    End If

End Sub

Private Function PaethPredictor(Left As Integer, Above As Integer, UpperLeft As Integer) As Integer

' http://www.w3.org/TR/PNG/#9-table91
' algorithm is used for both encoding & decoding the png paeth filter
' Based off of the formula created by Alan W. Paeth & provided fully in url above


    Dim pa As Integer, pb As Integer, pC As Integer, p As Integer
    p = Left + Above - UpperLeft
    pa = Abs(p - Left)
    pb = Abs(p - Above)
    pC = Abs(p - UpperLeft)
    
    ' tie breaker
    ' The order in which the comparisons are performed is critical and shall not be altered
    If (pa <= pb) And (pa <= pC) Then
        PaethPredictor = Left
    ElseIf pb <= pC Then
        PaethPredictor = Above
    Else
        PaethPredictor = UpperLeft
    End If

End Function

Private Function FilterImage(fileNum As Long, Stream() As Byte, Host As c32bppDIB, ByVal FilterMethod As eFilterMethods) As Boolean

    ' Routine will Filter the image in one of the 5 types of authorized PNG Filters.
    ' The Adaptive filter approach will select a best-guess filter to use for each
    ' scan line of the image. Otherwise, the same filter is applied to every scan line.
    
    ' Note about filters. Binary data compresses very poorly. Filters are a way to
    '   rewrite the binary data so that it will compress better. That is its only purpose.


    Dim scanWidth_DIB As Long, scanWidth_PNG As Long    ' scanwidths of 2 images
    Dim compressedData() As Byte          ' filtered PNG data
    Dim filteredData() As Byte           ' unfiltered PNG data
    Dim gpLong As Long              ' general purpose Long value
    Dim arrayPtr As Long, pIndex As Long    ' array/loop variables
    Dim bytePP As Byte              ' DIB/PNG bytes per pixel
    
    If FilterMethod < filterDefault Or FilterMethod > filterAdaptive Then FilterMethod = filterDefault

    Select Case m_ColorType
    Case clrGrayScale, clrPalette
        scanWidth_DIB = Host.Width
        scanWidth_PNG = scanWidth_DIB
        ' paletted images. Almost always, filter type zero is best (no filters)
        If FilterMethod = filterDefault Then FilterMethod = filterNone
        bytePP = 1
        
    Case Else ' true color, true color w/alpha (grayscale w/Alpha is converted to clrPalette in PalettizeImage)
        ' the best, non-adapative method for 24/32 bit is usually Paeth
        If FilterMethod = filterDefault Then FilterMethod = filterPaeth
        If m_ColorType = clrTrueAlpha Then
            ' get scan width for PNG file: byte aligned
            scanWidth_DIB = Host.scanWidth
            scanWidth_PNG = scanWidth_DIB
            bytePP = 4
        Else
            scanWidth_DIB = iparseByteAlignOnWord(24, Host.Width)
            scanWidth_PNG = ByteAlignOnByte(Host.Width, 24)
            bytePP = 3
        End If

    End Select
    
    ' Size raw data to be compressed and include 1 filter byte per line of image
    ReDim filteredData(0 To scanWidth_PNG * Host.Height + Host.Height - 1)
    If Err Then
        ' about the only possible error would be not enough memory to process the image file
        Err.Clear
        Exit Function
    End If
    
    
        For pIndex = 0 To Host.Height - 1
            
            arrayPtr = pIndex * scanWidth_PNG + pIndex   ' position of scanline
            
            If FilterMethod = filterAdaptive Then
                ' adaptive filtering
                ' although this can sequeeze an extra couple kb out of the png, I am finding
                ' that using Paeth appears to be either better or very close to adaptive filtering
                ' in most cases. Paeth is slowest of the top 5 filters (0-4). But adaptive filtering
                ' is significantly slower than Paeth. The deciding factor for adapative
                ' outdoing the others is the number of colors in the image. The more colors,
                ' the better chances adaptive has of being smaller size. The least amount
                ' of colors, the better chances Paeth has of being smaller.
                
                ' More testing needed though. I wouldn't imagine the PNG specs would recommend
                ' adaptive filtering unless it had some huge advantage over Paeth. What
                ' I am avoiding at all costs is a brute force routine to definitively
                ' find the best scanline filter method. That brute force can literally
                ' take minutes on full size 24/32bpp images.
                
                filteredData(arrayPtr) = 0
                gpLong = scanWidth_PNG * 254&  ' max value
                
                ' listed in order of quickest
                EncodeFilter_None filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, gpLong
                EncodeFilter_Sub filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, gpLong
                EncodeFilter_Up filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, gpLong
                EncodeFilter_Avg filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, gpLong
                EncodeFilter_Paeth filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, gpLong
                ' ^^ the -scanWidth_DIB is a flag indicating we are not filtering interlaced PNG
            Else
                filteredData(arrayPtr) = FilterMethod - 1
            End If
                    
            Select Case filteredData(arrayPtr) + 1 ' cache filter method into PNG data
            Case filterNone
                gpLong = (Host.Height - pIndex - 1) * scanWidth_DIB ' get current row relative to upside down DIB
                CopyMemory filteredData(arrayPtr + 1), m_Uncompressed(gpLong), scanWidth_PNG
            Case filterAdjLeft
                EncodeFilter_Sub filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, 0
            Case filterAdjTop
                EncodeFilter_Up filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, 0
            Case filterAdjAvg
                EncodeFilter_Avg filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, 0
            Case filterPaeth
                EncodeFilter_Paeth filteredData, pIndex, Host.Height - pIndex - 1, -scanWidth_DIB, scanWidth_PNG, bytePP, 0
            End Select
            ' ^^ the -scanWidth_DIB is a flag indicating we are not filtering interlaced PNG
        
        Next
    
    Erase m_Uncompressed    ' no longer needed
    If Err Then
        Err.Clear
    Else
        gpLong = UBound(filteredData) + 1
        gpLong = gpLong * 0.01 + 12 + gpLong ' < sizing algorithm for compressed array per zLIB specs
        ReDim compressedData(0 To gpLong + 3) ' include 4 bytes for Write_IDAT
        If zDeflate(VarPtr(compressedData(4)), gpLong, VarPtr(filteredData(0)), UBound(filteredData) + 1) = True Then
            FilterImage = Write_IDAT(fileNum, Stream(), compressedData(), gpLong)
        End If
    
    End If

End Function

Private Function FormatText(txt2format As String) As String
    
    ' Function ensures text added to a PNG file meets PNG specs
    Dim Index As Integer
    Dim rtnString As String
    Const maxLength As Long = 32700&
    ' not a PNG restriction, but self-imposed. Keep text to Integer length
    
    ' per PNG specs, text and captions are limited to Latin1 character set and the line feed
    ' Latin1 character set is Chr$(32) & above.
    ' http://www.w3.org/TR/PNG/iso_8859-1.txt
    
    If txt2format = vbNullString Then
        rtnString = txt2format
    Else
        rtnString = Left$(txt2format, maxLength)
        
        ' per PNG specs, the only character allowed lower than a SPACE is the line feed character
        ' therefore we will replace vbCrLf with the line feed chr$(10)/vbLf
        rtnString = Replace$(rtnString, vbCrLf, vbLf)
        rtnString = Replace$(rtnString, vbCr, vbLf)
        
        For Index = 1 To Len(rtnString)
            Select Case Asc(Mid$(rtnString, Index, 1))
            Case 9, 10   ' lf is valid. Tab, depending on sources, is ok too
            Case Is < 32 ' all these are not allowed
                rtnString = vbNullString
                Exit For
            Case Else       ' otherwise, all other characters are allowed
            End Select
        Next
    End If
    
    FormatText = rtnString

End Function

Private Function FormatCaption(ByRef Caption As String) As Long

    ' per PNG specs, text and captions are limited to Latin1 character set and the line feed
    ' Latin1 character set is Chr$(32) & above.
    ' http://www.w3.org/TR/PNG/iso_8859-1.txt
    
    ' Return values
    ' If caption is a reserved caption,
    '       then FormatCaption=PropertyID of reserved caption
    '   else FormatCaption=-1 to indicate not reserved
    ' If caption is disqualified, then Caption is returned as vbNullString

    Dim Index As Long, CaptionID As Long
    
    If Not Caption = vbNullString Then
        
        ' we are formatting/validating a keyword/caption for LargeBlockText
            
        ' when adding txtLargeBlock, a keyword is required, but must meet specs
        Caption = Trim$(Left$(Caption, 79)) ' absolute requirement
        Do Until InStr(Caption, "  ") = 0   ' absolute requirement
            Caption = Replace$(Caption, "  ", " ") ' remove all double spaces
        Loop
        
        'only character codes 32-126 and 161-255 are allowed
        For Index = 1 To Len(Caption)
            Select Case Asc(Mid$(Caption, Index, 1))
            Case 160: Mid$(Caption, Index, 1) = Chr$(32) ' suggested: convert hard space to soft space
            Case 32 To 126  ' valid
            Case 161 To 255  ' valid
            Case Else       ' otherwise, all other characters are NOT allowed, invalidating caption
                Caption = vbNullString
                Exit For
            End Select
        Next
        
        ' now the last check, cannot use a reserved keyword
        If Not Caption = vbNullString Then CaptionID = isKeyWord(Caption)
    
    End If

    FormatCaption = CaptionID

End Function

Private Function isKeyWord(inCaption As String) As Long

    ' compares passed caption to PNG reserved keywords
    Dim Index As Long, keyWord As String, keyID As Long
        For Index = 1 To 11
            Select Case Index
            Case 1: keyWord = "Title": keyID = txtTitle
            Case 2: keyWord = "Author": keyID = txtAuthor
            Case 3: keyWord = "Description": keyID = txtDescription
            Case 4: keyWord = "Copyright": keyID = txtCopyright
            Case 5: keyWord = "Creation Time": keyID = txtCreationTime
            Case 6: keyWord = "Software": keyID = txtSoftware
            Case 7: keyWord = "Disclaimer": keyID = txtDisclaimer
            Case 8: keyWord = "Warning": keyID = txtWarning
            Case 9: keyWord = "Source'": keyID = txtSource
            Case 10: keyWord = "Comment": keyID = txtComment
            Case 11: keyID = -1: Exit For
            End Select
            If StrComp(keyWord, inCaption, vbTextCompare) = 0 Then Exit For
        Next
    isKeyWord = keyID   ' return value of -1 indicates caption is not reserved
    
End Function

Private Function IsValidProperty(PropertyID As ePngProperties) As Boolean
    Select Case PropertyID
    Case txtAuthor: IsValidProperty = True
    Case txtComment: IsValidProperty = True
    Case txtCopyright: IsValidProperty = True
    Case txtCreationTime: IsValidProperty = True
    Case txtDescription: IsValidProperty = True
    Case txtDisclaimer: IsValidProperty = True
    Case txtLargeBlockText: IsValidProperty = True
    Case txtSoftware: IsValidProperty = True
    Case txtSource: IsValidProperty = True
    Case txtTitle: IsValidProperty = True
    Case txtWarning: IsValidProperty = True
    Case colorDefaultBkg: IsValidProperty = True
    Case filterType: IsValidProperty = True
    Case dateTimeModified: IsValidProperty = True
    End Select
    
End Function

' =======================================
' FOLLOWING 3 FUNCTIONS ARE ZLIB RELATED
' =======================================

Private Function zValidateZLIBversion() As Boolean

    ' Test for zlib availability & compatibility
    ' see modParsers.iparseValidateZLib for details
    
    Dim b_cdecl As Boolean, bCompress2 As Boolean, DllName As String
    
    If iparseValidateZLIB(DllName, m_ZLIBver, b_cdecl, bCompress2) = True Then
        If b_cdecl = True Then
            Set cCfunction = New cCDECL
            cCfunction.DllLoad DllName
        End If
        If bCompress2 Then m_ZLIBver = m_ZLIBver Or 32 ' flag indicating can use better compression
        zValidateZLIBversion = True
    End If
            
    
End Function

Private Function zCreateCRC(crcSrcRef As Long, srcLength As Long) As Long

    ' function returns zLIB's CRC value for passed crcTestRef value.
    Dim lReturn As Long
    If cCfunction Is Nothing Then
        If (m_ZLIBver And 1&) = 1& Then
            lReturn = Zcrc32(0&, ByVal crcSrcRef, srcLength)
        ElseIf (m_ZLIBver And 2&) = 2& Then
            lReturn = Zcrc321(0&, ByVal crcSrcRef, srcLength)
        End If
    Else
        lReturn = cCfunction.CallFunc("crc32", 0&, crcSrcRef, srcLength)
    End If
    If Not lReturn = 0& Then zCreateCRC = iparseReverseLong(lReturn)
    
End Function

Private Function zDeflate(destRef As Long, destSize As Long, srcRef As Long, srcSizeRef As Long) As Boolean

    ' function compresses/deflates passed srcRef into passed destRef and modifies the destSizeRef to indicate byte count of destRef
    
    ' earliest versions of DLL do not have Compress2 which newer versions have.
    ' Newer versions allow a compression parameter to allow deeper compression.
    ' When compress is called in newer DLL it just reroutes to the compress2 method
    
    Dim lReturn As Long
    If cCfunction Is Nothing Then
        If m_ZLIBver = 34& Then ' 34 = 2 or 32                  ' double checked 3/1/2007
            ' use compress2 function
            zDeflate = (Zcompress21(ByVal destRef, destSize, ByVal srcRef, srcSizeRef, zlibMaxCompression) = 0&)
        ElseIf m_ZLIBver = 33& Then ' 33 = 1 or 32              ' double checked 3/1/2007
            ' use compress2 function
            zDeflate = (Zcompress2(ByVal destRef, destSize, ByVal srcRef, srcSizeRef, zlibMaxCompression) = 0&)
        ElseIf (m_ZLIBver And 1) = 1& Then                      ' double checked 3/1/2007
            ' use compress function
            zDeflate = (Zcompress(ByVal destRef, destSize, ByVal srcRef, srcSizeRef) = 0&)
        ElseIf m_ZLIBver = 2& Then                              ' double checked 3/1/2007
            ' use compress function
            zDeflate = (Zcompress1(ByVal destRef, destSize, ByVal srcRef, srcSizeRef) = 0&)
        End If
    Else
        If (m_ZLIBver And 32&) = 32& Then                       ' double checked 3/1/2007
            ' use compress2 function
            zDeflate = (cCfunction.CallFunc("compress2", destRef, VarPtr(destSize), srcRef, srcSizeRef, zlibMaxCompression) = 0&)
        Else                                                    ' double checked 3/1/2007
            ' use compress function
            zDeflate = (cCfunction.CallFunc("compress", destRef, VarPtr(destSize), srcRef, srcSizeRef) = 0&)
        End If
    End If

End Function


